package org.andengine.util.adt.bit;

import org.andengine.util.adt.data.constants.DataConstants;
import org.andengine.util.exception.MethodNotYetImplementedException;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Arrays;


/**
 * (c) 2011 Zynga Inc.
 *
 * @author Nicolas Gramlich <ngramlich@zynga.com>
 * @since 15:56:32 - 30.10.2011
 */
public class LongBackedBitVector extends BitVector {
    // ===========================================================
    // Constants
    // ===========================================================

    // ===========================================================
    // Fields
    // ===========================================================

    private final int mSize;
    private final long[] mData;

    // ===========================================================
    // Constructors
    // ===========================================================

    public LongBackedBitVector(final int pSize) throws IllegalArgumentException {
        if (pSize <= 0) {
            throw new IllegalArgumentException("pSize must be > 0.");
        }
        this.mSize = pSize;

		/* Check if bytes perfectly fit into the data fields or if there are some overflow bytes that need special treatment. */
        final boolean perfectDataFit = (pSize % DataConstants.BITS_PER_LONG) == 0;

        final int dataLength;
        if (perfectDataFit) {
            dataLength = pSize / DataConstants.BITS_PER_LONG;
        } else {
            /* Extra field for overflow bytes. */
            dataLength = (pSize / DataConstants.BITS_PER_LONG) + 1;
        }
        this.mData = new long[dataLength];
    }

    public LongBackedBitVector(final long[] pData) throws IllegalArgumentException {
        throw new MethodNotYetImplementedException();
    }

    public LongBackedBitVector(final int pSize, final long[] pData) throws IllegalArgumentException {
        if (pData == null) {
            throw new IllegalArgumentException("pData must not be null");
        }

        if (pSize > (pData.length * Long.SIZE)) {
            throw new IllegalArgumentException("pData is too short.");
        }

        if (BitVector.calculateLongSize(pSize) < pData.length) {
            throw new IllegalArgumentException("pData is too long.");
        }

        throw new MethodNotYetImplementedException();
    }

    public LongBackedBitVector(final byte[] pData) throws IllegalArgumentException, NullPointerException {
        this(pData.length * DataConstants.BITS_PER_BYTE);

        final long[] data = this.mData;

		/* Check if bytes perfectly fit into the data fields or if there are some overflow bytes that need special treatment. */
        final boolean perfectDataFit = (pData.length % DataConstants.BYTES_PER_LONG) == 0;

        final int dataLength = data.length;
        final int lastCompleteDataIndex = (perfectDataFit) ? dataLength - 1 : dataLength - 2;

        for (int i = lastCompleteDataIndex; i >= 0; i--) {
            final int bytesOffset = i * DataConstants.BYTES_PER_LONG;

            data[i] = ((((long) pData[bytesOffset + 0]) << 56) & 0xFF00000000000000L)
                    | ((((long) pData[bytesOffset + 1]) << 48) & 0xFF000000000000L)
                    | ((((long) pData[bytesOffset + 2]) << 40) & 0xFF0000000000L)
                    | ((((long) pData[bytesOffset + 3]) << 32) & 0xFF00000000L)
                    | ((((long) pData[bytesOffset + 4]) << 24) & 0xFF000000L)
                    | ((((long) pData[bytesOffset + 5]) << 16) & 0xFF0000L)
                    | ((((long) pData[bytesOffset + 6]) << 8) & 0xFF00L)
                    | ((((long) pData[bytesOffset + 7]) << 0) & 0xFFL);
        }

		/* Put overflow bytes into last data field. */
        if (!perfectDataFit) {
            long overflowData = 0;

            final int overflowDataIndex = dataLength - 1;
            final int overflowBytesOffset = overflowDataIndex * DataConstants.BYTES_PER_LONG;

            final int overflowByteCount = pData.length - overflowBytesOffset;
            switch (overflowByteCount) {
                case 7:
                    overflowData = overflowData | ((((long) pData[overflowBytesOffset + 6]) << 8) & 0xFF00L);
                case 6:
                    overflowData = overflowData | ((((long) pData[overflowBytesOffset + 5]) << 16) & 0xFF0000L);
                case 5:
                    overflowData = overflowData | ((((long) pData[overflowBytesOffset + 4]) << 24) & 0xFF000000L);
                case 4:
                    overflowData = overflowData | ((((long) pData[overflowBytesOffset + 3]) << 32) & 0xFF00000000L);
                case 3:
                    overflowData = overflowData | ((((long) pData[overflowBytesOffset + 2]) << 40) & 0xFF0000000000L);
                case 2:
                    overflowData = overflowData | ((((long) pData[overflowBytesOffset + 1]) << 48) & 0xFF000000000000L);
                case 1:
                    overflowData = overflowData | ((((long) pData[overflowBytesOffset + 0]) << 56) & 0xFF00000000000000L);
            }

            data[overflowDataIndex] = overflowData;
        }
    }

    public LongBackedBitVector(final DataInputStream pDataInputStream) throws IOException {
        this.mSize = pDataInputStream.readInt();
        this.mData = new long[BitVector.calculateLongSize(this.mSize)];

        throw new MethodNotYetImplementedException();

//		pDataInputStream.readFully(this.mData);
    }

    public static LongBackedBitVector load(final DataInputStream pDataInputStream) throws IOException {
        return new LongBackedBitVector(pDataInputStream);
    }

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    @Override
    public int getSize() {
        return this.mSize;
    }

    @Override
    public int getBit(final int pIndex) throws IllegalArgumentException {
        if (pIndex < 0) {
            throw new IllegalArgumentException("pIndex must be >= 0.");
        }
        if (pIndex >= this.mSize) {
            throw new IllegalArgumentException("pIndex must be < size.");
        }

        final int dataIndex = pIndex / DataConstants.BITS_PER_LONG;
        final int dataOffset = pIndex % DataConstants.BITS_PER_LONG;

        final long dataField = this.mData[dataIndex];

        final int rightShift = DataConstants.BITS_PER_LONG - dataOffset - 1;
        final long dataBit = (dataField >> rightShift) & 0x01;
        final int bit = (int) dataBit;

        return bit;
    }

    @Override
    public boolean getBitAsBoolean(final int pIndex) throws IllegalArgumentException {
        return this.getBit(pIndex) == BitVector.TRUE;
    }

    @Override
    public void setBit(final int pIndex) throws IllegalArgumentException {
        throw new MethodNotYetImplementedException();
    }

    @Override
    public void clearBit(final int pIndex) throws IllegalArgumentException {
        throw new MethodNotYetImplementedException();
    }

    @Override
    public void setBit(final int pIndex, final boolean pTrue) throws IllegalArgumentException {
        throw new MethodNotYetImplementedException();
    }

    @Override
    public int getBits(final int pIndex, final int pLength) throws IllegalArgumentException {
        throw new MethodNotYetImplementedException();
    }

    @Override
    public long getLongBits(final int pIndex, final int pLength) throws IllegalArgumentException {
        /* Sanity checks. */
        if (pIndex < 0) {
            throw new IllegalArgumentException("pIndex must be >= 0.");
        }
        if (pLength < 0) {
            throw new IllegalArgumentException("pLength must be >= 0.");
        }
        if ((pIndex + pLength) > this.mSize) {
            throw new IllegalArgumentException("pIndex + pLength must be <= size.");
        }

		/* Early exit. */
        if (pLength == 0) {
            return 0L;
        }

        final int dataIndex = pIndex / DataConstants.BITS_PER_LONG;
        final int offset = pIndex % DataConstants.BITS_PER_LONG;

        final long data;
        if (offset == 0) {
            data = this.mData[dataIndex];
        } else if ((offset + pLength) <= DataConstants.BITS_PER_LONG) {
            data = this.mData[dataIndex] << offset;
        } else {
			/* Join bits from adjacent data fields. */
            data = (this.mData[dataIndex] << offset) | (this.mData[dataIndex + 1] >>> (DataConstants.BITS_PER_LONG - offset));
        }

        if (pLength == DataConstants.BITS_PER_LONG) {
            return data;
        } else {
            final int rightShift = DataConstants.BITS_PER_LONG - pLength;
            final long mask = 0xFFFFFFFFFFFFFFFFL >>> rightShift;
            final long unmaskedBits = data >> rightShift;
            return unmaskedBits & mask;
        }
    }


    @Override
    public void setBits(final int pIndex, final byte pBits, final int pBitIndex, final int pBitCount) throws IllegalArgumentException {
        throw new MethodNotYetImplementedException();
    }

    @Override
    public void setBits(final int pIndex, final short pBits, final int pBitIndex, final int pBitCount) throws IllegalArgumentException {
        throw new MethodNotYetImplementedException();
    }

    @Override
    public void setBits(final int pIndex, final int pBits, final int pBitIndex, final int pBitCount) throws IllegalArgumentException {
        throw new MethodNotYetImplementedException();
    }

    @Override
    public byte getByte(final int pIndex) throws IllegalArgumentException {
        return (byte) this.getBits(pIndex, DataConstants.BITS_PER_BYTE);
    }

    @Override
    public void setByte(final int pIndex, final byte pByte) throws IllegalArgumentException {
        throw new MethodNotYetImplementedException();
    }

    @Override
    public short getShort(final int pIndex) throws IllegalArgumentException {
        return (short) this.getBits(pIndex, DataConstants.BITS_PER_SHORT);
    }

    @Override
    public void setShort(final int pIndex, final short pShort) throws IllegalArgumentException {
        throw new MethodNotYetImplementedException();
    }

    @Override
    public int getInt(final int pIndex) throws IllegalArgumentException {
        return this.getBits(pIndex, DataConstants.BITS_PER_INT);
    }

    @Override
    public void setInt(final int pIndex, final int pInt) throws IllegalArgumentException {
        throw new MethodNotYetImplementedException();
    }

    @Override
    public long getLong(final int pIndex) throws IllegalArgumentException {
        return this.getLongBits(pIndex, DataConstants.BITS_PER_LONG);
    }

    @Override
    public void setLong(final int pIndex, final long pLong) throws IllegalArgumentException {
        throw new MethodNotYetImplementedException();
    }

    @Override
    public void clear() {
        Arrays.fill(this.mData, 0x00);
    }

    @Override
    public void fill(final byte pByte) {
        throw new MethodNotYetImplementedException();
    }

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================

    @Override
    public void save(final DataOutputStream pDataOutputStream) throws IOException {
        throw new MethodNotYetImplementedException();
    }

    @Override
    public byte[] toByteArray() {
        final int byteArrayLength;
        if ((this.mSize % DataConstants.BITS_PER_BYTE) == 0) {
            byteArrayLength = this.mSize / DataConstants.BITS_PER_BYTE;
        } else {
            byteArrayLength = (this.mSize / DataConstants.BITS_PER_BYTE) + 1;
        }

        final byte[] bytes = new byte[byteArrayLength];
		/* Check if bytes perfectly fit into the data fields or if there are some overflow bytes that need special treatment. */
        final boolean perfectDataFit = (this.mSize % DataConstants.BITS_PER_LONG) == 0;

        final long[] data = this.mData;
        final int dataLength = data.length;
        final int lastCompleteDataIndex = (perfectDataFit) ? dataLength - 1 : dataLength - 2;

        int bytesOffset = (lastCompleteDataIndex * DataConstants.BYTES_PER_LONG) + (DataConstants.BYTES_PER_LONG - 1);
        for (int i = lastCompleteDataIndex; i >= 0; i--) {
            final long dataField = data[i];

            bytes[bytesOffset--] = (byte) ((dataField >> 0) & 0xFF);
            bytes[bytesOffset--] = (byte) ((dataField >> 8) & 0xFF);
            bytes[bytesOffset--] = (byte) ((dataField >> 16) & 0xFF);
            bytes[bytesOffset--] = (byte) ((dataField >> 24) & 0xFF);
            bytes[bytesOffset--] = (byte) ((dataField >> 32) & 0xFF);
            bytes[bytesOffset--] = (byte) ((dataField >> 40) & 0xFF);
            bytes[bytesOffset--] = (byte) ((dataField >> 48) & 0xFF);
            bytes[bytesOffset--] = (byte) ((dataField >> 56) & 0xFF);
        }

		/* Put overflow bytes into last data field. */
        if (!perfectDataFit) {
            final int overflowDataIndex = dataLength - 1;
            final long overflowDataField = data[overflowDataIndex];

            final int overflowBytesOffset = overflowDataIndex * DataConstants.BYTES_PER_LONG;

            final int overflowByteCount = bytes.length % DataConstants.BYTES_PER_LONG;
            switch (overflowByteCount) {
                case 7:
                    bytes[overflowBytesOffset + 6] = (byte) ((overflowDataField >> 8) & 0xFF);
                case 6:
                    bytes[overflowBytesOffset + 5] = (byte) ((overflowDataField >> 16) & 0xFF);
                case 5:
                    bytes[overflowBytesOffset + 4] = (byte) ((overflowDataField >> 24) & 0xFF);
                case 4:
                    bytes[overflowBytesOffset + 3] = (byte) ((overflowDataField >> 32) & 0xFF);
                case 3:
                    bytes[overflowBytesOffset + 2] = (byte) ((overflowDataField >> 40) & 0xFF);
                case 2:
                    bytes[overflowBytesOffset + 1] = (byte) ((overflowDataField >> 48) & 0xFF);
                case 1:
                    bytes[overflowBytesOffset + 0] = (byte) ((overflowDataField >> 56) & 0xFF);
            }
        }

        return bytes;
    }

    @Override
    public String toString() {
        final StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append('[');

        for (int i = 0; i < this.mSize; i++) {
            if (this.getBit(i) == BitVector.TRUE) {
                stringBuilder.append('1');
            } else {
                stringBuilder.append('0');
            }

            if (((i % Byte.SIZE) == (Long.SIZE - 1)) && (i < (this.mSize - 1))) {
                stringBuilder.append(',').append(' ');
            }
        }

        stringBuilder.append(']');
        return stringBuilder.toString();
    }

    // ===========================================================
    // Methods
    // ===========================================================

    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================
}